﻿using System;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;

namespace ExpressionMapper
{
    using MappingExpressionGenerator = Func<Expression, Type, Expression, ParameterExpression, Expression>;

    /// <summary>
    /// Returns generator for Dictionary to Dictionary mappings.
    /// </summary>
    public static partial class Generators
    {
        public static MappingExpressionGenerator StackToStack()
        {
            return (fromExpr, to, customMapping, referenceDictonary) =>
                   (IsEnumerable(fromExpr.Type) && IsStack(to))
                       ? customMapping ?? GenerateStackConversionExpression(fromExpr, to, referenceDictonary)
                       : null;
        }

        private static Expression GenerateStackConversionExpression(Expression fromExpr,Type to, ParameterExpression referenceDictonary)
        {
            Debug.WriteLine("\tStackToStack ({0}): {1} to {2}", fromExpr.NodeType, fromExpr.Type.Name,
                            to.Name);

          
            Type fromElementType = GetCollectionElementType(fromExpr.Type);
            Type toElementType = GetCollectionElementType(to);

            string methodName = null;
            if (IsGenericStack(to))
            {
                methodName = "ConvertGenericStack";
            }
            if (IsObjectStack(to))
            {
                methodName = "ConvertObjectStack";
            }
            if (IsConcurrentStack(to))
            {
                methodName = "ConvertConcurrentStack";
            }

            if (string.IsNullOrEmpty(methodName))
                throw new InvalidOperationException(string.Format("Could not find conversion method for type {0}", to.FullName));

            MethodInfo convertAllMethod = typeof(Generators)
                .GetMethod(methodName, BindingFlags.Static | BindingFlags.Public)
                .MakeGenericMethod(fromElementType, toElementType);

            return Expression.Call(convertAllMethod, fromExpr, referenceDictonary);
        }

        private static bool IsStack(Type t)
        {
            return IsConcurrentStack(t) || IsGenericStack(t) || IsObjectStack(t);
        }

        private static bool IsConcurrentStack(Type t)
        {
            return (t.IsGenericType && t.GetGenericTypeDefinition() == typeof(ConcurrentStack<>)) 
                //|| typeof(ConcurrentStack<>).IsAssignableFrom(t)
                ;
        }

        private static bool IsObjectStack(Type t)
        {
            return t == typeof (Stack) 
                //|| typeof (Stack).IsAssignableFrom(t)
                ;
        }

        private static bool IsGenericStack(Type t)
        {
            return (t.IsGenericType && t.GetGenericTypeDefinition() == typeof(Stack<>)) 
                //|| typeof(Stack<>).IsAssignableFrom(t)
                ;
        }

        public static Stack<TOutput> ConvertGenericStack<TInput, TOutput>(IEnumerable<TInput> collection, ReferenceDictionary referenceDictionary)
        {
            if (collection == null)
            {
                return null;
            }

            var enumerable = ConvertEnumerable<TInput, TOutput>(collection, referenceDictionary);
            if (!IsStack(collection.GetType()))
                enumerable = enumerable.Reverse();
            return new Stack<TOutput>(enumerable);
        }

        public static Stack ConvertObjectStack<TInput>(IEnumerable<TInput> collection, ReferenceDictionary referenceDictionary)
        {
            if (collection == null)
            {
                return null;
            }

            var enumerable = ConvertEnumerable<TInput, object>(collection, referenceDictionary);
            if (!IsStack(collection.GetType()))
                enumerable = enumerable.Reverse();

            Stack result = new Stack();
            foreach (var input in enumerable)
            {
                result.Push(input);
            }
            return result;
        }

        public static ConcurrentStack<TOutput> ConvertConcurrentStack<TInput, TOutput>(IEnumerable<TInput> collection
                                                                                            , ReferenceDictionary referenceDictionary)
        {
            if (collection == null)
            {
                return null;
            }

            var enumerable = ConvertEnumerable<TInput, TOutput>(collection, referenceDictionary);
            if (!IsStack(collection.GetType()))
                enumerable = enumerable.Reverse();
            return new ConcurrentStack<TOutput>(enumerable);
        }
    }
}